package com.darkflame.client.query;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.logging.Logger;

import com.darkflame.client.SuperSimpleSemantics;
import com.darkflame.client.interfaces.GenericQueryDisplayer;
import com.darkflame.client.semantic.SSSNode;
import com.darkflame.client.semantic.SSSProperty;


/** Contains all the Nodes & Data needed for a query
 * Also contains routines to process string querys into query objects **/
public class Query extends ArrayList<QueryElement>{

	static Logger Log = Logger.getLogger("sss.Query");
	
	/** A simple enum for choosing between OR querys and AND querys */
	public enum QueryMode {
		AND,
		OR
	};
	
	/** If set to true, And querys become Nand querys<br>
	 *  OR querys become Nor querys **/
	public Boolean invertResults = false; 
	
	/** Determines what type of query this is.
	 * We default to an AND type query <br>
	 * Note; Within a single query you can only have one type of request <br>
	 * eg. Fruit AND Green AND Round<br>
	 * Fruit OR Green OR Round <br>
	 * To mix and match you need to use subquerys. **/
	public QueryMode querymode = QueryMode.AND; 
	
	/** The associated displayer is the visual widget that will show, and thus help
	 * debug, this querys results.
	 * This is very usefull when you have a bunch of subquerys and want to see 
	 * what each one contributes ***/
	public GenericQueryDisplayer assoociatedDisplayer;

	
	private static HashMap<String,Query> knownquerys = new HashMap<String,Query> ();

	/** an object representing a query to run on the database 
	 * Its made up of a mode; Simple AND or OR
	 * and a collection of query elements, which typicaly represent the requirements all
	 * the nodes (AND) or any of the nodes (OR) have to meet.
	 * A query element can also, however, be a subquery.**/
	public Query(QueryMode querymode,ArrayList<QueryElement> allnodestotest) {
		super(allnodestotest);
		this.querymode = querymode;
	}
	
	/** NOTE; references to this should be replaced with createQuerySafely to stop duplicate creations and thus redundant query deductions
	 * 
	 * we attempt to make sense of the users query from a string.<br>
	 * Spaces are interpreted as "AND" use quotes if you want to include spaces as literal characters
	 *  <br>
	 * ie. on a uri with a space in it, always quote it! <br>
	 *  <br>
	 * Use = for searching for property "colour=green" and brackets for subquerys<br>
	 * eg <br>
	 * fruit (colour=red || colour=green)<br>
	 * <br>
	 * Would give green or red fruit<br>
	 * 
	 *  **/
	public Query(String usersRawEntry){

		processRawQuery(usersRawEntry);
		
		//associate the input string with the parsed query
		//so we can retrieve it again if the same query is asked for
		//note, as this is a crude string match with "_!_" at the start if its a negative look up
		//This negative prefix is probably bad, but I cant think how to do it better right now
		knownquerys.put(usersRawEntry,this);
		
		
		
	}
	
	/** creates a new query, or returns if theres a matching existing one 
	 * @return **/
	public static Query createQuerySafely(String usersRawEntry){
		
		
		return createQuerySafely(usersRawEntry,false);
		
	}
	/** creates a new query, or returns if there's a matching existing one 
	 * @return **/
	public static Query createQuerySafely(String usersRawEntry, boolean invert){
		String invertflag ="";
		if (invert){
			 invertflag = "_!_";
		}
				
		if (knownquerys.containsKey(invertflag+usersRawEntry)){
			Log.info("matching existing query");
			
			return knownquerys.get(invertflag+usersRawEntry);
		} else {
			return new Query(usersRawEntry,invert);
		}		
		
	}
	

	/**  NOTE; references to this should be replaced with createQuerySafely to stop duplicate creations and thus redundant query deductions
	 * 
	 *  we attempt to make sense of the users query from a string.<br>
	 * Spaces are interpreted as "AND" use quotes if you want to include spaces as literal characters
	 * Use = for searching for property "colour=green" and brackets for subquerys<br>
	 * eg <br>
	 * fruit (colour=red || colour=green)<br>
	 * <br>
	 * Would give green or red fruit<br>
	 * 
	 *  **/
	public Query(String usersRawEntry, boolean invert){
		invertResults=invert;
		processRawQuery(usersRawEntry);
		

		//associate the input string with the parsed query
		//so we can retrieve it again if the same query is asked for
		//note, as this is a crude string match with "_!_" at the start if its a negative look up
		//This negative prefix is probably bad, but I cant think how to do it better right now
		String invertflag ="";
		if (invert){
			 invertflag = "_!_";
		}
		knownquerys .put(invertflag+usersRawEntry,this);
	}
	
	/** returns the query as a human readable(ish) string **/
	public String getAsString(){
		
		
		String result="";
		
		for (QueryElement qe : this) {
			
			result=result+" "+querymode.toString()+" "+qe.getAsString();
			
			
			
		}
		return result;
	}
	
	
	/** returns all the nodes used in this query and any subquerys within it.
	 * This might be useful for debugging, or for tidying up query results when you dont
	 * want the results to also contain subclass's that defined them.
	 * ie "Apple" might result in "Granny Smith,Coxs,Apple" 
	 * This would let you remove that last Apple from the list**/
	public HashSet<SSSNode> allUsedNodes(){
		
		HashSet<SSSNode> nodesused = new HashSet<SSSNode>();
		
		for (QueryElement qe : this) {
			//loop over all elements getting their used nodes
			nodesused.addAll(qe.getAllUsedNodes());
		}
		
		return nodesused;
	}
	
	/** This function takes a string of the user, and splits it into an array of words, 
	 * respecting quote marks for words with spaces.
	 * eg,<br>
	 * 'one twenty "one hundred" 666 lots'<br>
	 * would give; <br>
	 * one<br>
	 * twenty<br>
	 * "one hundred"<br>
	 * 666<br>
	 * lots <br>**/
	//Note, recently made static. Didnt see the need for it not to be
	static public ArrayList<String> splitRawQueryToWords(String usersInputMess){
		
		
		SuperSimpleSemantics.info("---------");
		SuperSimpleSemantics.info("------splitting '"+usersInputMess+"' to seperate words ---");
		SuperSimpleSemantics.info("---------");
		
		//first we insert spaces around all brackets as this makes processing easier
		usersInputMess=usersInputMess.replace("(", " ( ");
		usersInputMess=usersInputMess.replace(")", " ) ");
		
		// we also add spaces to  "=" and "||"  for the same reason
		usersInputMess=usersInputMess.replace("=", " = ");
		usersInputMess=usersInputMess.replace("!", " ! ");
		
		usersInputMess=usersInputMess.replace("||", " || ");
		
		int totalchat = usersInputMess.length();
		Boolean inQuotes = false;
		ArrayList<String> words = new  ArrayList<String>();		 
		String currentWord = "";
		
		for (int i = 0; i < totalchat; i++) {
			
			char currentLetter = usersInputMess.charAt(i);
		
			//if its a quote, then we toggle inQuotes
			if (currentLetter=='"'){
				inQuotes=!inQuotes;
			}
			
			//if we are not in quotes, and at a space, we can assume a word is finnished and thus
			//add it to the array
			if ((!inQuotes)&&((currentLetter==' '))){
												
				if (currentWord.trim()!=""){
					words.add(currentWord.trim());
					SuperSimpleSemantics.info("word added::  "+currentWord);
				}
				currentWord = "";
			} else {
				//continue making the word
				currentWord = currentWord + currentLetter;
				
			}
			
			
			
		}
		
		//add last word
		if (currentWord.trim()!=""){
		SuperSimpleSemantics.info("word added::  "+currentWord);
		words.add(currentWord.trim());
		}
		
		
		
		
		return words;
		
	}

	
	/** Builds this query from the users words
	 * note sure why this is returning a string, probably doesnt have too**/
	public String processRawQuery(String usersEntry) {

		SuperSimpleSemantics.info("processing word set::  "+usersEntry);
		
		//first we separate all words out, respecting quotes
		ArrayList<String> wordlist = splitRawQueryToWords(usersEntry);
				
	//	Log.info("Words found:"+wordlist.toString());
		
		
		int nestedQuery=0; //outer most layer
		
		String nestedQueryString = "";
		String previousword="";
		String currentWord="";
		boolean neg = false;
		
		//then we loop over all words
		for (int i=0; i<wordlist.size();i++) {
			
			currentWord = wordlist.get(i);
			
			SuperSimpleSemantics.info("current word::"+currentWord);
			SuperSimpleSemantics.info("previous word::"+previousword);
			
			if ((currentWord.equals("!"))&&(nestedQuery==0)){

				SuperSimpleSemantics.info("--- negative detected ---");
				//next subquery is negative
				neg=true;
				continue;
				
			}
			
			if (currentWord.equals("(")){

				SuperSimpleSemantics.info("( detected nestquery level="+nestedQuery);
				nestedQuery++;
				
			}
			if (currentWord.equals(")")){
				
				nestedQuery--;
				
				SuperSimpleSemantics.info("nestedQuery::  "+nestedQuery);
				
				if (nestedQuery==0){
					
					nestedQueryString=nestedQueryString+" "+currentWord;
										
					if (neg){
						SuperSimpleSemantics.info("subquery adding::__!"+nestedQueryString);
					} else {
						SuperSimpleSemantics.info("subquery adding::__"+nestedQueryString);
					}
					
				//remove brackets and process
					nestedQueryString=nestedQueryString.trim();
					nestedQueryString=nestedQueryString.substring(1, nestedQueryString.length()-1);
				
					//we make a new element for our query from this data
					QueryElement newQueryElement = new QueryElement(new Query(nestedQueryString,neg));
				
					this.add(newQueryElement);					
					neg=false;				
					
					nestedQueryString="";
					SuperSimpleSemantics.info("------------(subquery added)"+newQueryElement.getAsString());							
					
					//we continue to the next word loop, unless the next word is a "="
					//Because = will use both the current added query, its simpler
					//to just go on.
					
					//can be done better
					
					//if the previous word equals "="
					//String nextword="";
					//if ((wordlist.size()>i+1)){
					//	nextword = wordlist.get(i+1);					
					//}
					
					if ((previousword.equals("="))){
											
						SuperSimpleSemantics.info("__subquery is after a property so we trigger adding a Property");		

						internalAddProperty(currentWord);

						previousword="";
						neg=false;
						continue;
					} else {

						SuperSimpleSemantics.info("continueing from subquery");							
						continue;
						
					}
					
					
					
					
				}
				
			}
			
			//if we are inside a bracket ( we wait till the closing one, and process that 
			//as a subquery
			if (nestedQuery!=0){
				nestedQueryString=nestedQueryString+" "+currentWord;
				continue;
			}
			
			
			if (currentWord.equals("||")){						
				querymode = QueryMode.OR;
				//skip to next word
				previousword="";
				continue;
				
			}
			
		//	if (currentWord.equals("=")){
				//we wait for the next loop to deal with it, as we need to know the 
				//next word to proceed
				//previousword=currentWord;
				//continue;
		//	}
			
			//if its 1 character we ignore it
			//if (currentWord.length()==1){
			//	previousword = currentWord;
			//	continue;
			//}
			//if its a = character we ignore it
			if (currentWord.equals("=")){				
				previousword = currentWord;
				continue;
			}
			
			
			//else we add the separate word
			SuperSimpleSemantics.info("currentWord adding:: "+currentWord);
			
			String word = currentWord;
			//if it does not contain : or "." assume its a label
			if (!word.contains(":")&&(!word.contains("."))){
				
				//we store as previous word before making any changes to this one
				//previous word=word;
				
				//remove quotes if there's any
				word = word.replace("\"", "");
				
				SSSNode requestednode = SSSNode.getNodeByLabel(word);
				if (requestednode!=null){
				word = requestednode.PURI;
				}
				
				//we make a new element for our query from this data
				QueryElement newQueryElement = new QueryElement(new SSSProperty(SSSNode.SubClassOf,requestednode));
				this.add(newQueryElement);
				
			} else {
				
				//we store as previous word before making any changes to this one
				//previousword=word;
				
				Log.info("get node by uri:"+word);
				SSSNode requestednode = SSSNode.getNodeByUri(word);
				
				if (requestednode!=null){
					word = requestednode.PURI;
				}
					
				//we make a new element for our query from this data
				QueryElement newQueryElement = new QueryElement(new SSSProperty(SSSNode.SubClassOf,requestednode));
				this.add(newQueryElement);
				
				
			}
			
			SuperSimpleSemantics.info("previousword ::  "+previousword);
			
			//This should only be after the VALUE in the triplet, so we cant triggered till after the "Y" in X=Y
			if (previousword.equals("=")){
				
				internalAddProperty(currentWord);
					
					previousword="";
				neg=false;
				continue;
				
			}
			
			
			
			
			
			//store previous word
			previousword = currentWord;
			
		}
	
		
		
		
		
		return null;
		
		
	}

	public void internalAddProperty(String currentWord) {
		SuperSimpleSemantics.info("adding triplet specification ::  ");
		 	
			//last QueryElement added
			QueryElement lastElement = this.get(this.size()-1);
			//the one before that 
			QueryElement oneBeforeThatElement = this.get(this.size()-2);
			
			Query predSubQuery = null;
			Query valSubQuery = null;
			
			String pred ="";
			String val="";
			
			SuperSimpleSemantics.info(" last queryelement="+lastElement.getAsString());
			SuperSimpleSemantics.info(" last element before that="+oneBeforeThatElement.getAsString());
			
			//if the one before Element was a subQuery, we store a flag
			if (oneBeforeThatElement.query!=null){
				SuperSimpleSemantics.info("element before last is query ");
				predSubQuery = oneBeforeThatElement.query;
			} 
			
			//if the lastElement was a subQuery, we store a flag
			if (lastElement.query!=null){
				SuperSimpleSemantics.info("lastElement is query ");
				valSubQuery = lastElement.query;
			} 
		
			//pred will be last value added unless its a query
			if (predSubQuery==null){
				pred = oneBeforeThatElement.prop.getValue().PURI;	
				//remove quotes if theres any
				pred = pred.replace("\"", "");
			}
			
			if (valSubQuery==null){
				//val will be current word unless its a query
				val = currentWord;
				val = val.replace("\"", "");
			}
			
			
			SuperSimpleSemantics.info("pred ::  "+pred);
			SuperSimpleSemantics.info("val ::  "+val);
			
			//remove the two last words from the list
			SuperSimpleSemantics.info("removing last entry"+this.size());
			
			this.remove(this.size()-1);
			this.remove(this.size()-1);
			
			//add based on type
			if (predSubQuery==null && valSubQuery==null){
				SuperSimpleSemantics.info("neither are querys ::  ");
				
				//Totally text man!
				addNewRequiredProperty(pred, val);
				
			} else if (predSubQuery!=null && valSubQuery==null) {
				
				//predicate is subquery
				SuperSimpleSemantics.info("pred is query ::  "+predSubQuery.getAsString());
				addNewRequiredProperty(predSubQuery, val);
				
			}	else if (predSubQuery==null && valSubQuery!=null) {
				
				//value is subquery
				SuperSimpleSemantics.info("val is query ::  "+valSubQuery.getAsString());						
				addNewRequiredProperty(pred, valSubQuery);
				
			}	else if (predSubQuery!=null && valSubQuery!=null) {
				
				//both are
				SuperSimpleSemantics.info("both are querys ::  "+valSubQuery.getAsString());
				addNewRequiredProperty(predSubQuery, valSubQuery);
				
			}
	}

	/** at property from querys uris **/
	public void addNewRequiredProperty(Query pred, Query val) {
				
		//we make a new element for our query from this data
		QueryElement newQueryElement = new QueryElement(new QuerySSSProperty(pred,val));
		this.add(newQueryElement);
	}
	
	/** at property from query and string uris **/
	public void addNewRequiredProperty(Query pred, String val) {
		SSSNode VAL = SSSNode.NOTFOUND;
		
		if (!val.contains(":")&&(!val.contains("."))){
			//if its a label
			VAL = SSSNode.getNodeByLabel(val);
		} else 					
		//if its already a url
		{
			VAL = SSSNode.getNodeByUri(val);
			
		}		
		
		//we make a new element for our query from this data
		QueryElement newQueryElement = new QueryElement(new QuerySSSProperty(pred,VAL));
		
		this.add(newQueryElement);
	}
	
	/** at property from query and string uris **/
	public void addNewRequiredProperty(String pred, Query val) {
		SSSNode PRED = SSSNode.NOTFOUND;
		
		if (!pred.contains(":")&&(!pred.contains("."))){
			//if its a label
			PRED = SSSNode.getNodeByLabel(pred);
		}else 					
			//if its already a url
			{
			PRED = SSSNode.getNodeByUri(pred);
				
			}
				
		//we make a new element for our query from this data
		QueryElement newQueryElement = new QueryElement(new QuerySSSProperty(PRED,val));
		this.add(newQueryElement);
	}
	
	/** at property from string uris **/
	public void addNewRequiredProperty(String pred, String val) {
		SSSNode PRED = SSSNode.NOTFOUND;
		SSSNode VAL = SSSNode.NOTFOUND;
		
		if (!pred.contains(":")&&(!pred.contains("."))){
			//if its a label
			PRED = SSSNode.getNodeByLabel(pred);
		}else 					
			//if its already a url
			{
			PRED = SSSNode.getNodeByUri(pred);
				
			}
		
		if (!val.contains(":")&&(!val.contains("."))){
			//if its a label
			VAL = SSSNode.getNodeByLabel(val);
		} else 					
		//if its already a url
		{
			VAL = SSSNode.getNodeByUri(val);
			
		}
		
		
		//we make a new element for our query from this data
		QueryElement newQueryElement = new QueryElement(new SSSProperty(PRED,VAL));
		this.add(newQueryElement);
	}
	


	
	/** returns true if the query has all its nodes recognised **/
	public boolean hasNoErrors() {
		
		//tests for any errors
		for (QueryElement qe : this) {			
			 if (qe.hasError()){
				 return false;
			 }			
		}
		
		return true;
	}

	/** used to help visualise results, you can assign a string representin results to a query or a subquery.
	 * If an associatedDisplayer is set, it will update it with the results **/
	public void setResultData(String data) {
		
		if (assoociatedDisplayer!=null){
			assoociatedDisplayer.setResultData(data);
		}
		
	}
	
	
	

}
